package com.malex.mxtracker.action;

import com.malex.mxtracker.MxTracker;
import com.malex.mxtracker.bean.Bank;
import com.malex.mxtracker.bean.Currency;
import com.malex.mxtracker.bean.IDataBaseObject;
import com.malex.mxtracker.bean.Operation;
import com.malex.mxtracker.bean.Project;
import com.malex.mxtracker.exception.WrongCommandException;
import com.malex.mxtracker.store.EM;
import com.malex.mxtracker.util.Utils;

import javax.persistence.EntityManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;

/**
 * @author Manusovich Alexander
 */
public class ActionHandler {
    private static final Logger log = Logger.getLogger(ActionHandler.class.getName());
    private String command;
    private String owner;

    public ActionHandler(String command) {
        this(command, "app");
    }

    public ActionHandler(String command, String owner) {
        this.command = command;
        this.owner = owner;
    }

    public ActionResult execute() throws WrongCommandException {
        List<String> result = new ArrayList<String>();

        if (command == null) {
            result.add("Command is not defined...");
            return new ActionResult(result);
        }

        String cmdString = command.trim().toLowerCase();
        List<String> tokens = Utils.getTokens(cmdString);
        if (tokens == null) {
            result.add("Command is not defined...");
            return new ActionResult(result);
        }

        log.info(String.format("action: %s from %s", command, owner));
        boolean operation = false;
        try {
            if (tokens.size() > 2) {
                Double.valueOf(tokens.get(1));
                operation = true;
            }
        } catch (NumberFormatException e) {
            //
        }
        if (operation) {
            /**
             * Регистрируем операцию
             */
            return registerOperation(tokens);
        } else {
            String gc = tokens.get(0);
            if ("uc".equals(gc)) {
                /**
                 * Обновление курса валют
                 */
                MxTracker.updateCurrencies(Utils.updateCurrencyDictionary());
                return new ActionResult("updated");
            } else if ("sc".equals(gc)) {
                /**
                 * Установка валюты по умлочанию
                 */
                String currCode = tokens.get(1);
                MxTracker.updateCurrencies(
                        Utils.updateCurrencyDictionary(currCode));
                return new ActionResult("updated");
            } else if ("mv".equals(gc)) {
                /**
                 * Перемещение денег с одного счета на другой
                 */
                transferMoney(tokens);
                return new ActionResult("moved");
            } else if ("add".equals(gc)) {
                /**
                 * Добавление данных в систему
                 */
                String subj = tokens.get(1);
                if ("b".equals(subj)) {
                    addBank(tokens);
                } else if ("p".equals(subj)) {
                    addProject(tokens);
                }

                return new ActionResult("added");
            } else if ("remove".equals(gc)) {
                /**
                 * Удаление данных из системы
                 */
                String subj = tokens.get(1);
                if ("b".equals(subj)) {
                    removeBank(tokens);
                } else if ("p".equals(subj)) {
                    removeProject(tokens);
                }
                return new ActionResult("removed");
            } else if ("cl".equals(gc)) {
                return reportCurrencies();
            } else if ("bl".equals(gc)) {
                return reportBanks();
            }
        }
        return new ActionResult("Command is not valid or doesn't supported");
    }

    private ActionResult registerOperation(final List<String> tokens) throws WrongCommandException {
        Double value = Double.valueOf(tokens.get(1));
        if ("remove".equals(tokens.get(0))) {
            value = -value;
        }
        String projectName;
        if (tokens.size() == 4) {
            projectName = tokens.get(3);
            Project p = getProjectByName(projectName);
            Bank b = MxTracker.getBanks().get(p.getBank());
            value = Utils.getMoney(value, tokens.get(2), b.getCurrency());
        } else {
            projectName = tokens.get(2);
        }
        Project p = getProjectByName(projectName);
        Bank b = MxTracker.getBanks().get(p.getBank());

        /**
         * Обновляем проект и счёт
         */
        b.setValue(b.getValue() + value);
        persist(b);

        if (value < 0) {
            p.setExpense(p.getExpense() + Math.abs(value));
        } else {
            p.setIncoming(p.getIncoming() + value);
        }
        persist(p);

        /**
         * Регистрируем операцию
         */
        Operation o = new Operation();
        o.setDate(new Date());
        o.setProject(p.getId());
        o.setValue(value);
        persist(o);

        ActionResult result = new ActionResult("Executed");
        result.getPesponseParams().put("project", "" + p.getId());
        return result;
    }

    private Project getProjectByName(final String projectName) throws WrongCommandException {
        Project project = (Project) getObjectByName(MxTracker.getProjects().values(), projectName);
        if (project == null) {
            throw new WrongCommandException(
                    "Project with name " + projectName + " was not found.");
        }
        return project;
    }

    private void addProject(final List<String> tokens) throws WrongCommandException {
        String name = tokens.get(2);
        checkSameName(name, Bank.class);
        Project p = new Project();
        p.setName(name);
        /**
         * Если параметров 6 - значит указан родитель
         */
        if (tokens.size() == 6) {
            String parentName = tokens.get(3);
            Project parent = getProjectByName(parentName);
            p.setParent(parent.getId());

        }
        String bankName = tokens.get(tokens.size() - 2);
        Bank bank = (Bank) getObjectByName(MxTracker.getBanks().values(), bankName);
        if (bank == null) {
            throw new WrongCommandException(
                    "Bank with name " + bankName + " was not found.");
        }
        p.setBank(bank.getId());
        p.setDescription(tokens.get(tokens.size() - 1));
        persist(p);
    }


    private static IDataBaseObject getObjectByName(Collection objects, String name) {
        for (Object o : objects) {
            if (o instanceof IDataBaseObject
                    && name.equals(((IDataBaseObject) o).getName())) {
                return (IDataBaseObject) o;
            }
        }
        return null;
    }

    private void removeBank(final List<String> tokens) throws WrongCommandException {
        String bankNameForRemove = tokens.get(2);
        for (Bank b : MxTracker.getBanks().values()) {
            if (b.getName().equals(bankNameForRemove)) {
                remove(b);
            }
        }
    }

    private void removeProject(final List<String> tokens) throws WrongCommandException {
        String projectForRemove = tokens.get(2);
        for (Project project : MxTracker.getProjects().values()) {
            if (project.getName().equals(projectForRemove)) {
                remove(project);
            }
        }
    }

    public static void remove(final IDataBaseObject... objs) throws WrongCommandException {
        EntityManager em = EM.get().createEntityManager();
        try {
            em.getTransaction().begin();
            for (IDataBaseObject o : objs) {
                em.remove(em.find(o.getClass(), o.getId()));
            }
            em.getTransaction().commit();
        } catch (Exception e) {
            em.getTransaction().rollback();
            throw new WrongCommandException(e.getMessage());
        } finally {
            em.close();
        }
    }

    private ActionResult reportCurrencies() {
        List<String> result = new ArrayList<String>();
        for (Currency c : MxTracker.getCurrencies().values()) {
            result.add(String.format("%s %f %s", c.getCharCode(), c.getRate(),
                    c.getDef() != null && c.getDef() ? "-default" : ""));
        }
        return new ActionResult(result);
    }

    private ActionResult reportBanks() {
        List<String> result = new ArrayList<String>();
        for (Bank b : MxTracker.getBanks().values()) {
            result.add(String.format("%s %f %s", b.getName(), b.getValue(), b.getCurrency()));
        }
        return new ActionResult(result);
    }

    /**
     * Перевод денег с одного счета на другой
     *
     * @param tokens Параметры
     * @throws WrongCommandException Ошибка
     */
    private void transferMoney(final List<String> tokens) throws WrongCommandException {
        Bank from = null, to = null;
        for (Bank b : MxTracker.getBanks().values()) {
            if (b.getName().equals(tokens.get(1))) {
                from = b;
            }
            if (b.getName().equals(tokens.get(2))) {
                to = b;
            }
        }
        if (from == null) {
            throw new WrongCommandException(
                    String.format("Source bank '%s' was not found...", tokens.get(1)));
        }
        if (to == null) {
            throw new WrongCommandException(
                    String.format("Destination bank '%s' was not found...", tokens.get(2)));
        }
        Double value = Utils.getDouble(tokens.get(3));
        String curr = tokens.get(4);
        /**
         * Проверяем есть ли сумма на счету from
         */
        Double vFrom = Utils.getMoney(value, curr, from.getCurrency());
        if (from.getValue() < vFrom) {
            Double vHave = Utils.getMoney(from.getValue(), from.getCurrency(), curr);
            throw new WrongCommandException(
                    String.format("Source bank '%s' doesn't have enough money (only %f %s)...",
                            from.getName(), vHave, curr));
        }
        /**
         * Вычисляем сколько будет на том счету куда переносим деньги
         */
        Double vTo = Utils.getMoney(value, curr, to.getCurrency());
        /**
         * Меняем состояние банков
         */
        from.setValue(from.getValue() - vFrom);
        to.setValue(to.getValue() + vTo);
        persist(from, to);
    }

    /**
     * Сохранение изменений в базу
     *
     * @param objs Список объектов
     * @throws com.malex.mxtracker.exception.WrongCommandException
     *          Ошмбка
     */
    public static void persist(final Object... objs) throws WrongCommandException {
        for (Object o : objs) {
            EntityManager em = EM.get().createEntityManager();
            try {
                em.getTransaction().begin();
                em.persist(o);
                em.refresh(o);
                em.getTransaction().commit();
            } catch (Exception e) {
                em.getTransaction().rollback();
                throw new WrongCommandException(e.getMessage());
            } finally {
                em.close();
            }
        }
    }

    /**
     * Добавление новго счета
     *
     * @param tokens Параметры
     * @throws WrongCommandException Ошибка
     */
    private void addBank(final List<String> tokens) throws WrongCommandException {
        String name = tokens.get(2);
        checkSameName(name, Bank.class);
        Bank bank = new Bank();
        bank.setName(name);
        bank.setValue(Utils.getDouble(tokens.get(3)));
        bank.setCurrency(tokens.get(4));
        persist(bank);
    }

    private void checkSameName(final String name,
                               final Class<Bank> clazz) throws WrongCommandException {
        String query = String.format("select from %s where name='%s'",
                clazz.getName(), name);
        EntityManager em = EM.get().createEntityManager();
        try {
            if (em.createQuery(query).getResultList().size() > 0) {
                throw new WrongCommandException(
                        String.format(clazz.getSimpleName() +
                                " with name '%s' is exists!", name));
            }
        } finally {
            em.close();
        }
    }
}
